Declarative Macros
Declarative macros (also called macro_rules macros) let you define pattern-based code generation. You describe what input patterns look like and what code they expand into.
They are:
- Matched at compile time
- Purely syntactic (no runtime cost)
- Great for eliminating repetitive code
Think of them as smart search-and-replace rules, but with structure and safety.
Basic Structure
macro_rules! macro_name {
(pattern) => {
expansion
};
}
You can define multiple rules inside one macro.
Example 1: A Simple Macro
macro_rules! say_hello {
() => {
println!("Hello, world!");
};
}
fn main() {
say_hello!();
}
How it works:
()matches no arguments.- When the compiler sees
say_hello!(), it replaces it with:
println!("Hello, world!");
Macro Variables and Fragment Specifiers
You can capture parts of the input using macro variables:
macro_rules! print_value {
($x:expr) => {
println!("The value is: {}", $x);
};
}
Here:
$xis a variable.expris a fragment specifier (tells Rust what kind of syntax to match).
Common fragment specifiers:
| Specifier | Matches |
|---|---|
expr | Expression |
ident | Identifier |
ty | Type |
pat | Pattern |
stmt | Statement |
block | Block { ... } |
item | Item (fn, struct, etc.) |
literal | Literal value |
path | Path like std::fmt::Debug |
tt | Token tree (most flexible) |
Example 2: Macro with One Argument
macro_rules! square {
($x:expr) => {
$x * $x
};
}
fn main() {
let a = square!(4);
let b = square!(3 + 2);
println!("{}, {}", a, b);
}
Expansion:
square!(4)→4 * 4square!(3 + 2)→(3 + 2) * (3 + 2)(conceptually)
Example 3: Multiple Patterns (Overloading)
macro_rules! log {
($msg:expr) => {
println!("LOG: {}", $msg);
};
($fmt:expr, $($arg:tt)*) => {
println!(concat!("LOG: ", $fmt), $($arg)*);
};
}
fn main() {
log!("Hello");
log!("x = {}, y = {}", 10, 20);
}
How it works:
- First rule matches one expression.
- Second rule matches a format string plus any number of extra tokens (arg:tt)*`).
Repetition Syntax
Repetition lets you match multiple inputs.
$(pattern),* // zero or more, separated by commas
$(pattern),+ // one or more
$(pattern)? // zero or one
Example 4: Variadic Macro (Like vec!)
Let’s reimplement a simplified vec! macro:
macro_rules! my_vec {
() => {
Vec::new()
};
($($x:expr),+ $(,)?) => {
{
let mut v = Vec::new();
$(
v.push($x);
)+
v
}
};
}
fn main() {
let a = my_vec![];
let b = my_vec![1, 2, 3];
let c = my_vec![10, 20, 30,];
}
()handles empty input.$($x:expr),+matches one or more expressions separated by commas.$(,)?allows an optional trailing comma.$()+repeats the code for each matched$x.
Example 5: Generating Code (Struct + Impl)
macro_rules! make_struct {
($name:ident) => {
struct $name;
impl $name {
fn new() -> Self {
$name
}
}
};
}
make_struct!(Foo);
fn main() {
let f = Foo::new();
}
Expansion:
struct Foo;
impl Foo {
fn new() -> Self {
Foo
}
}
This shows how macros can generate items, not just expressions.
Hygiene (Why Macros Are Safe)
Rust macros are hygienic, meaning:
- Variables defined inside a macro won’t accidentally clash with variables outside it.
- Names resolve to the correct scope.
macro_rules! make_var {
() => {
let x = 10;
println!("{}", x);
};
}
fn main() {
let x = 5;
make_var!(); // prints 10, not 5
}
The macro’s x is distinct from the outer x.
When to Use macro_rules!
Use declarative macros when:
- You want to eliminate repetitive boilerplate.
- You need syntax that functions can’t express (e.g., variadic arguments).
- You want zero runtime cost abstractions.
Avoid them when:
- The logic becomes too complex or unreadable.
- A function, trait, or generic type would work just as well.